/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.stdlib.time.type;

import axil.api.*;
import axil.definition.UnitDefinition;
import axil.engine.Repository;
import axil.framework.localization.Locality;
import axil.framework.localization.Translation;
import axil.framework.type.AbstractUnitizedValue;
import axil.framework.type.Added;
import axil.framework.type.Mathematical;
import axil.framework.type.Subtracted;
import axil.stdlib.core.type.Nil;
import axil.stdlib.numbers.type.Int;
import axil.stdlib.numbers.type.Num;

import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;

import static axil.framework.Functions.*;


/**
 * An object representing a span of time.
 */
public class Interval extends AbstractUnitizedValue<Interval>
	implements Added, Subtracted, Mathematical, Formatted {
	public enum Span {
		millisecond,
		second,
		minute,
		hour,
		day,
		month,
		week,
		quarter,
		year
	}

	public static final Interval_Type type = new Interval_Type();
	private static Map<UnitDefinition,Span> map = null;


	private Num value;
	private UnitDefinition units;
	private Span span;


	public Interval(Num value, UnitDefinition units) {
		assert value != null;
		assert units != null;

		this.value = value;
		this.units = units;
	}


	public Interval(Num value, Span span) {
		assert value != null;
		assert span != null;

		this.value = value;
		this.units = type.unit(span.name());
		this.span = span;
	}


	/**
	 * Get the type metadata for this object. The type metadata contains
	 * information about the type itself, as an object.
	 */
	public AxilType type() {
		return type;
	}


	/**
	 * Get the Axil base value within this unitized value. This is the magnitude
	 * or raw value without any unit information.
	 */
	public Num magnitude() {
		return value;
	}


	/**
	 * Get the units for this unitized value. The units returned are never null.
	 */
	public UnitDefinition units() {
		return units;
	}


	/**
	 * Return the host application (or built-in Java) object that most closely
	 * matches this value object. Return returned value is never null unless
	 * this value object represents the special 'nil' object.
	 */
	public Object intrinsic() {
		return this;
	}


	/**
	 * Get the enumerated type that matches the units. Since date/time math is
	 * complex, we cannot use the built-in conversion mechanisms.
	 */
	public Span span() {
		if (span == null) {
			if (map == null) {
				Map<UnitDefinition,Span> m = new HashMap<UnitDefinition, Span>();
				for (Span k : Span.values()) {
					UnitDefinition u = Repository.instance().unit(k.name());
					if (u == null) {
						throw abort(
						    "The interval type does not define a recognizable " +
						    "unit of this type. This is an internal error and " +
						    "a defect.",
						    nv("units", k));
					}
					m.put(u, k);
				}
				span = m.get(units);
				map = m;
			} else {
				span = map.get(units);
			}
		}
		return span;
	}


	/**
	 * Return this interval as a whole number of days. The value returned is
	 * always greater than or equal to zero. If the value is a fractional number
	 * of days, then standard rounding is applied.
	 */
	public int days() {
		return (int)as(type.days()).magnitude().whole();
	}


	/**
	 * Return the canonical representation of a zero value in this numeric
	 * type.
	 */
	public Num zero() {
		return Int.zero;
	}


	/**
	 * Add the given value to this value, returning a new object. The given
	 * value is never null or nil. The value may be a numeric type of any other
	 * type of object that can sensibly implement the Added interface.
	 */
	public AxilObject add(AxilObject object) {
		if (object == Nil.object) {
			return Nil.object;
		}
		if (object instanceof Interval) {
			Interval other = (Interval)object;
			return new Interval((Num)value.add(other.as(units).magnitude()), units);
		}
		if (object instanceof Num) {
			return new Interval((Num)value.add(object), units);
		}
		throw error(axil(), "expecting-interval-type",
		            nv("type", object.type()));
	}


	/**
	 * Subtract the given object from this object. The given value is never
	 * null or nil. The value may be a numeric type of any other type of object
	 * that can sensibly implement the Subtracted interface.
	 */
	public AxilObject subtract(AxilObject object) {
		if (object == Nil.object) {
			return Nil.object;
		}
		if (object instanceof Interval) {
			Interval other = (Interval)object;
			return new Interval((Num)value.subtract(other.as(units).magnitude()), units);
		}
		if (object instanceof Num) {
			return new Interval((Num)value.subtract(object), units);
		}
		throw error(axil(), "expecting-interval-type",
		            nv("type", object.type()));
	}


	/**
	 * Multiply the given number by this number. This method can safely assume
	 * that the given value is not null or nil, but can be any of the numeric
	 * types.
	 */
	public Num multiply(Num n) {
		return new Interval(value.multiply(n), units);
	}


	/**
	 * Divide this number by the given number. This method can safely assume
	 * that the given value is not null or nil, but can be any of the numeric
	 * types.
	 */
	public Num divide(Num n) {
		return new Interval(value.divide(n), units);
	}


	/**
	 * Round this value according to the rules of the given rounding mode and
	 * to the given number of significant fractional digits. A value of zero
	 * rounds this to a whole number.
	 */
	public Num round(RoundingMode mode, int digits) {
		return new Interval(value.round(mode, digits), units);
	}


	/**
	 * Returns a hash code value for the object. All Axil values must provide a
	 * meaningful implementation of this function to enable themselves to be
	 * placed in a map or set.
	 */
	protected int hash() {
		return value.hashCode();
	}


	/**
	 * Tell if this object is equivalent to the given object. The object given
	 * is never null. The object given may be of any type of value object.
	 * Objects of incompatible types return a false value, not an exception.
	 */
	public boolean equalTo(AxilObject object) {
		if (object instanceof Interval) {
			Interval i = (Interval)object;
			if (this.units == i.units) {
				return value.equalTo(i.value);
			}
			return value.equalTo(i.as(units).magnitude());
		}
		return false;
	}


	/**
	 * Compares this object with the specified object for order. If the given
	 * object is not a suitable type for comparison, a ClassCastException may
	 * be thrown.
	 *
	 * @param object
	 * 	The object to compare against. The given object cannot be null but may
	 * 	be any Axil object.
	 *
	 * @return
	 * 	Returns a negative integer, zero, or a positive integer as this object
	 * 	is less than, equal to, or greater than the specified object.
	 */
	public int comparedTo(AxilObject object) {
		if (object == Nil.object) {
			return 1;
		}
		if (object instanceof Interval) {
			Interval other = (Interval)object;
			if (this.units == other.units) {
				return value.comparedTo(other.value);
			}
			return value.comparedTo(other.as(units).magnitude());
		}
		if (object instanceof Num) {
			return value.comparedTo(object);
		}
		return -1;
	}


	/**
	 * Format this object in a locale-friendly manner. The string returned is
	 * a localized representation of this object using the locale and timezone
	 * associated with this persona. The formatting given is never null and
	 * matches formatting suitable for this type.
	 */
	public String format(Persona persona, Formatting formatting) {
		// TODO: This is a great place to use the new plurality mechanism in templates, such as "1 second" or "3 seconds"
        //          Update TimeTest.testIntervalFormat() when include this support.
		Translation t = axil().translation("interval." + span(),
		                                   Locality.from(persona));
		return magnitude().format(persona, formatting) + ' ' +
		       t.text(persona, (int)value.whole());
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		return value.toString() + ' ' + units.identity();
	}
}
